﻿using System;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Web.Mvc;
using System.Web.Security;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using DotNetOpenAuth.OpenId.RelyingParty;

namespace Wheels.Controllers
{
    [HandleError]
    public class AccountController : Controller
    {
        private static readonly OpenIdRelyingParty openid = new OpenIdRelyingParty();

        // This constructor is used by the MVC framework to instantiate the controller using
        // the default forms authentication and membership providers.
        public AccountController()
            : this(null, null)
        {
        }

        // This constructor is not used by the MVC framework but is instead provided for ease
        // of unit testing this type. See the comments at the end of this file for more
        // information.
        public AccountController(IFormsAuthentication formsAuth, IMembershipService service)
        {
            FormsAuth = formsAuth ?? new FormsAuthenticationService();
            MembershipService = service ?? new AccountMembershipService();
        }

        public IFormsAuthentication FormsAuth { get; private set; }

        public IMembershipService MembershipService { get; private set; }

        public ActionResult LogIn()
        {
            // Stage 1: display login form to user
            return View();
        }

        [ValidateInput(false)]
        public ActionResult Authenticate(string returnUrl)
        {
            IAuthenticationResponse response = openid.GetResponse();
            if (response == null)
            {
                // Stage 2: user submitting Identifier
                Identifier id;
                if (Identifier.TryParse(Request.Form["openid_identifier"], out id))
                {
                    try
                    {
                        IAuthenticationRequest request = openid.CreateRequest(Request.Form["openid_identifier"]);
                        request.AddExtension(new ClaimsRequest
                                                 {
                                                     //FullName = DemandLevel.Request,
                                                     Email = DemandLevel.Require,
                                                     //Country = DemandLevel.Request,
                                                     //PostalCode = DemandLevel.Request,
                                                     //TimeZone = DemandLevel.Request
                                                 });
                        return request.RedirectingResponse.AsActionResult();
                    }
                    catch (ProtocolException ex)
                    {
                        ViewData["Message"] = ex.Message;
                        return View("Login");
                    }
                }
                else
                {
                    ViewData["Message"] = "Invalid identifier";
                    return View("Login");
                }
            }
            else
            {
                // Stage 3: OpenID Provider sending assertion response
                switch (response.Status)
                {
                    case AuthenticationStatus.Authenticated:
                        MembershipUser user = MembershipService.CreateOrGetUser(response);
                        FormsAuth.SignIn(user.UserName, true);

                        if (!string.IsNullOrEmpty(returnUrl))
                        {
                            return Redirect(returnUrl);
                        }
                        else
                        {
                            return RedirectToAction("Index", "Home");
                        }
                    case AuthenticationStatus.Canceled:
                        ViewData["Message"] = "Canceled at provider";
                        return View("Login");
                    case AuthenticationStatus.Failed:
                        ViewData["Message"] = response.Exception.Message;
                        return View("Login");
                }
            }
            return new EmptyResult();
        }

        public ActionResult LogOut()
        {
            FormsAuth.SignOut();
            return RedirectToAction("Index", "Home");
        }

        // TODO: do we need this? find out and remove if not.
        protected override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            if (filterContext.HttpContext.User.Identity is WindowsIdentity)
            {
                throw new InvalidOperationException("Windows authentication is not supported.");
            }
        }
    }

    // The FormsAuthentication type is sealed and contains static members, so it is difficult to
    // unit test code that calls its members. The interface and helper class below demonstrate
    // how to create an abstract wrapper around such a type in order to make the AccountController
    // code unit testable.

    public interface IFormsAuthentication
    {
        void SignIn(string userName, bool createPersistentCookie);
        void SignOut();
    }

    public class FormsAuthenticationService : IFormsAuthentication
    {
        #region IFormsAuthentication Members

        public void SignIn(string userName, bool createPersistentCookie)
        {
            FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
        }

        public void SignOut()
        {
            FormsAuthentication.SignOut();
        }

        #endregion
    }

    public interface IMembershipService
    {
        MembershipUser CreateOrGetUser(IAuthenticationResponse response);
    }

    public class AccountMembershipService : IMembershipService
    {
        private static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider();
        private readonly MembershipProvider provider;

        public AccountMembershipService()
            : this(null)
        {
        }

        public AccountMembershipService(MembershipProvider provider)
        {
            this.provider = provider ?? Membership.Provider;
        }

        #region IMembershipService Members

        public MembershipUser CreateOrGetUser(IAuthenticationResponse response)
        {
            MembershipUser user = provider.GetUser(response.ClaimedIdentifier, true);

            if (user == null)
            {
                var claimsResponse = response.GetExtension<ClaimsResponse>();
                string email = null;
                if (claimsResponse != null)
                {
                    email = claimsResponse.Email;
                    //fullName = claimsResponse.FullName;
                    //nickname = claimsResponse.Nickname;
                }

                MembershipCreateStatus status;
                user = provider.CreateUser(response.ClaimedIdentifier,
                                           GenerateRandomString(64),
                                           email,
                                           "This is an OpenID account. You should log in with your OpenID.",
                                           GenerateRandomString(64),
                                           true,
                                           null,
                                           out status);
                if (status != MembershipCreateStatus.Success)
                {
                    throw new Exception("Failed to find or create user: " + status);
                }

                // TODO: set extra info in the profile, taking it from OpenID.
            }
            return user;
        }

        #endregion

        private static string GenerateRandomString(int length)
        {
            var buffer = new byte[length];
            CryptoRandomDataGenerator.GetBytes(buffer);
            return Convert.ToBase64String(buffer);
        }
    }
}